﻿///-----------------------------------------------------------------
///   Namespace:            Neuron.Cell
///   Class:                Perceptron
///   Description:          This file contains an perceptron implementation based on the articles form wikipedia
///   Author:               Jens Möller
///   Date:                 19th August 2015
///   Notes:                Also integrated in this file are the enum of the three possible aktivation functions
///                         and the class for input-vector 
///-----------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Windows.Forms;
using System.Xml;

namespace Neuron.Cell
{

    // A public enum containing the different type of the activation function
    public enum ActivationFunctionEnum { hlim, sig, pwl, receptor, actor, trigger, buffer };

    // A public enum containing the different possibilities for an OnNeuron
    public enum OnNeuronEnum { withOnNeuron, withoutOnNeuron };

    // Definition of the input vector class which is used to bind the information of the weighting to 
    // incoming signals of the perceptron
    #region input vector item class
    class inputVectorItem
    {

        private Perceptron _perceptron = null;
        private double _weight = 0.0;

        public inputVectorItem(Perceptron p, double weight)
        {
            _perceptron = p;
            _weight = weight;
        }

        public Perceptron SenderPerceptron
        {
            get { return _perceptron; }
        }

        public double Weight
        {
            get { return _weight; }
            set { _weight = value; }
        }

        public void multiplyWeight(double value)
        {
            _weight = _weight * value;
        }

        public void incWeight(double value)
        {
            _weight += value;
        }

        public void decWeight(double value)
        {
            _weight -= value;
        }

    }
    #endregion

    // The perceptron class itself
    #region Perceptron class
    public class Perceptron
    {
        #region internal variables
        // internal variable definitions
        private double _hardLimit = 0;
        private double _vectorOUT = 0.0;
        private double _inputVectorValue = 0.0;

        // internal variable for a possible OnNeuron
        private Perceptron _onNeuron = null;
        private bool _onNeuronConnected = false;

        private ActivationFunctionEnum _currentActivationFunction = ActivationFunctionEnum.hlim;

        // input vector, to generate more than one output vector just add the current perceptron to
        // several input vectors of several other perceptrons
        private List<inputVectorItem> _vectorIN = new List<inputVectorItem>();

        private Control.ctrlPerceptron _control = null;

        // Name of the perceptron which is also shown in the ctrlPerceptron-Control
        private string _name = "";

        // Internal variable for the last calculated delta of the Criteria-Function
        // (Backpropagation-Result)
        private double _criteriaDelta = 0.0;

        // Initialdata - used for reset-function and restoring the original data
        private double _initHardLimit = 0.0;
        private ActivationFunctionEnum _initActivationFunction = ActivationFunctionEnum.hlim;
        private OnNeuronEnum _initOnNeuron = OnNeuronEnum.withoutOnNeuron;
        private string _initName = "";

        // ...is counting the learning steps
        private long _learningCounter = 0;
        // 20150913 - JMR - Self-Cloning-Function disabled
        //private bool _growingNetwork = false;

        // Latenztimingvariablen
        private long _latencySemaphore = 0;
        private long _latency = 100;

        // Feedback-Flag
        // Feedback wird nur empfangen und verarbeitet, wenn das Perceptron zwischenzeitlich getriggert wurde
        private bool _readyForFeedback = false;

        private DelayBuffer _dBuffer = null;
        private Trigger _trigger = null;

        #endregion

        #region constructor 
        // Constructor
        // Parameter:
        // HardLimit            = The hard limit for the activation function if the hlim-Function is used
        //                      = For the sig-Function it is the Sig-Faktor
        //                      = For the pwl-Function it is also the Limit (following Wikipedia it needs to be 0.5)
        public Perceptron(PerceptronBackbone backbone, string name, double HardLimit, ActivationFunctionEnum ActivationFunction, OnNeuronEnum OnNeuron)
        {
            this._name = name;
            this._learningCounter = 0;

            this._currentActivationFunction = ActivationFunction;

            this._trigger = new Trigger(1000);
            this._trigger.TriggerEvent += _trigger_TriggerEvent;
            this._dBuffer = new DelayBuffer(2000);
            this._dBuffer.ActionEvent += _dBuffer_ActionEvent;

            switch (this._currentActivationFunction)
            {
                case ActivationFunctionEnum.trigger:
                    this._hardLimit = 1.0;
                    this._onNeuronConnected = false;
                    this._onNeuron = null;
                    this._trigger.Start();
                    break;
                case ActivationFunctionEnum.buffer:
                    this._hardLimit = 0.5;
                    this._onNeuronConnected = false;
                    this._onNeuron = null;
                    this._dBuffer.Start();
                    break;
                case ActivationFunctionEnum.receptor:
                    this._hardLimit = 1.0;
                    this._onNeuronConnected = false;
                    this._onNeuron = null;
                    break;
                case ActivationFunctionEnum.actor:
                    this._hardLimit = HardLimit;
                    this._onNeuronConnected = false;
                    this._onNeuron = null;
                    break;
                default:
                    this._hardLimit = HardLimit;
                    // Definition for the OnNeuron if requested
                    if (OnNeuron == OnNeuronEnum.withOnNeuron)
                    {
                        this.AddOnNeuron();
                    }
                    break;
            }

            this._initName = this.Name;
            this._initActivationFunction = this._currentActivationFunction;
            this._initOnNeuron = OnNeuron;
            this._initHardLimit = this._hardLimit;

            if (backbone != null)
            {
                backbone.add(this);
            }
        }

        public void StartDelayBuffer()
        {
            this._dBuffer.Start();
        }

        public void StartTrigger()
        {
            this._trigger.Start();
        }

        public void Shutdown()
        {
            this._trigger.Stop();
            this._dBuffer.Stop();
        }

        private void _trigger_TriggerEvent()
        {
            if (this.ActivationFunction == ActivationFunctionEnum.trigger)
            {
                if (this._vectorOUT > 0)
                {
                    this.stimulate(0);
                }
                else
                {
                    this.stimulate(1.0);
                }
            }
        }

        private void _dBuffer_ActionEvent(double status)
        {
            if (this.ActivationFunction == ActivationFunctionEnum.buffer)
            {
                if (status > 0)
                {
                    this.stimulate(1.0);
                }
                else
                {
                    this.stimulate(0.0);
                }
            }

        }

        private void reset()
        {
            this._name = this._initName;
            this._currentActivationFunction = this._initActivationFunction;
            this._hardLimit = this._initHardLimit;

            if ((this._initOnNeuron == OnNeuronEnum.withOnNeuron)&&(this._onNeuronConnected == false))
            {
                this.AddOnNeuron();
                return;
            }
            if ((this._initOnNeuron == OnNeuronEnum.withoutOnNeuron) && (this._onNeuronConnected == true))
            {
                this.RemoveOnNeuron();
                return;
            }
        }

        #endregion

        #region OnNeuron handling (bias)
        public void AddOnNeuron()
        {
            this._onNeuron = new Perceptron(null, "ON_" + this._name, 1.0, ActivationFunctionEnum.hlim, OnNeuronEnum.withoutOnNeuron);

            inputVectorItem newItem = new inputVectorItem(this._onNeuron, 1);
            newItem.SenderPerceptron.FireAction += this.FireEvent;
            this._vectorIN.Insert(0, newItem);

            this._onNeuron.stimulate(1.0);
            this._onNeuronConnected = true;
        }

        public void RemoveOnNeuron()
        {
            inputVectorItem subNeuron = this._vectorIN[0];
            subNeuron.SenderPerceptron.FireAction -= this.FireEvent;
            subNeuron = null;
            this._vectorIN.RemoveAt(0);
            this._onNeuronConnected = false;
        }
        #endregion


        #region Property access methods

        // 20150913 - JMR - Self-Cloning-Function disabled
        /*
        public bool AllowGrowing
        {
            get { return this._growingNetwork; }
            set { this._growingNetwork = value; }
        }
        */

        public double CriteriaDelta
        {
            get { return this._criteriaDelta; }
        }

        public String Name
        {
            get { return this._name; }
        }
        
        public void Reset(double hardlimit, double WeightingForAll)
        {
            this.setWeightForAll(WeightingForAll);
            this._hardLimit = hardlimit;
        }

        public bool OnNeuronConnected
        {
            get { return _onNeuronConnected; }
        }
        
        public ActivationFunctionEnum ActivationFunction
        {
            get { return this._currentActivationFunction;  }
            set { this._currentActivationFunction = value;  }
        }
        
        // Property to access the internal _hadLimit-Variable
        public double HardLimit
        {
            get { return _hardLimit; }
            set { _hardLimit = value; }
        }

        // Property to access the output vector. This property is readonly
        public double VectorOUT
        {
            get { return this._vectorOUT; }
        }

        // Property to read the total value of the input vector when it was calculated 
        // the last time
        public double VectorIN
        {
            get { return _inputVectorValue; }
        }

        // Needs an index of a perceptron connected to the input vector of the current perceptron.
        // Return the weighting of the connected perceptron by this perceptron
        public double GetWeightingOf(int inputVectorItemNo)
        {
            if ((inputVectorItemNo < 0)||(inputVectorItemNo >= _vectorIN.Count()))
            {
                return -99.99;
            }
            else
            {
                return _vectorIN[inputVectorItemNo].Weight;
            }
        }

        // Sets the weighting of the connected perceptron
        public void SetWeightingOf(int inputVectorItemNo, double weight)
        {
            if ((inputVectorItemNo < 0) || (inputVectorItemNo >= _vectorIN.Count()))
            {
                return;
            }
            else
            {
                _vectorIN[inputVectorItemNo].Weight = weight;
            }
        }


        // Returns the number of perceptrons in the inputVector
        public int NumberOfInputNeurons
        {
            get { return this._vectorIN.Count(); }
        }

        //Returns an XML-String representing the Perceptron
        public void getXMLElement(XmlDocument xmlDoc, XmlElement parentControl, bool inclWeighting)
        {
            XmlElement XmlElementPerceptron = xmlDoc.CreateElement("Perceptron");

            XmlElementPerceptron.SetAttribute("Name", this.Name);
            XmlElementPerceptron.SetAttribute("ActivationFunction", this.ActivationFunction.ToString());
            XmlElementPerceptron.SetAttribute("HardLimit", Convert.ToString(this._hardLimit));
            XmlElementPerceptron.SetAttribute("BiasNeuron", Convert.ToString(this._onNeuronConnected));
            foreach (inputVectorItem inputItem in this._vectorIN)
            {
                XmlElement inputPerceptron = xmlDoc.CreateElement(inputItem.SenderPerceptron.Name);
                if (inclWeighting)
                {
                    inputPerceptron.SetAttribute("weight", Convert.ToString(inputItem.Weight));
                }
                else
                {
                    inputPerceptron.SetAttribute("weight", "1");
                }
                XmlElementPerceptron.AppendChild(inputPerceptron);
            }

            parentControl.AppendChild(XmlElementPerceptron);

            return;
        }


        // Needs an index of a perceptron connected to the input vector of the current perceptron.
        public Perceptron GetPerceptronAt(int inputVectorItemNo)
        {
            if ((inputVectorItemNo < 0) || (inputVectorItemNo >= _vectorIN.Count()))
            {
                return null;
            }
            else
            {
                return this._vectorIN[inputVectorItemNo].SenderPerceptron;
            }
        }

        public bool InputVectorRemovePerceptronFrom(Perceptron p)
        {
            bool result = false;
            this.FireAction -= p.FireEvent;

            int i = 0;
            while ((result == false) && (i < this._vectorIN.Count()))
            {
                inputVectorItem inItem = this._vectorIN[i];
                if (inItem.SenderPerceptron == p)
                {
                    this._vectorIN.RemoveAt(i);
                    p = null;
                    result = true;
                }
                i++;
            }
            return result;
        }

        public void InputVectorClear()
        {
            this._vectorIN.Clear();
        }

        // Needs an index of a perceptron connected to the input vector of the current perceptron.
        // Return the weighting of the connected perceptron by this perceptron
        public double InputVectorGetWeightingOfPerceptronAtIndex(int inputVectorItemNo)
        {
            if ((inputVectorItemNo < 0) || (inputVectorItemNo >= _vectorIN.Count()))
            {
                return -99.99;
            }
            else
            {
                return _vectorIN[inputVectorItemNo].Weight;
            }
        }

        // Returns the number of perceptrons in the inputVector
        public int InputVectorNumberOfInputNeurons
        {
            get { return this._vectorIN.Count(); }
        }

        // Needs an index of a perceptron connected to the input vector of the current perceptron.
        public Perceptron InputVectorGetPerceptronAtIndex(int inputVectorItemNo)
        {
            if ((inputVectorItemNo < 0) || (inputVectorItemNo >= _vectorIN.Count()))
            {
                return null;
            }
            else
            {
                return this._vectorIN[inputVectorItemNo].SenderPerceptron;
            }
        }

        // Needs an index of a perceptron connected to the input vector of the current perceptron.
        /*
        public double GetWeightingOfPerceptronAt(int inputVectorItemNo)
        {
            if ((inputVectorItemNo < 0) || (inputVectorItemNo >= _vectorIN.Count()))
            {
                return 0;
            }
            else
            {
                return this._vectorIN[inputVectorItemNo].Weight;
            }
        }
        */

        public bool RemovePerceptronFromInputVector(Perceptron p)
        {
            bool result = false;
            int i = 0;
            while ((result == false)&&(i < this._vectorIN.Count()))
            {
                inputVectorItem inItem = this._vectorIN[i];
                if (inItem.SenderPerceptron == p)
                {
                    this._vectorIN.RemoveAt(i);
                    p = null;
                    result = true;
                }
                i++;
            }
            return result;
        }

        public Control.ctrlPerceptron Control
        {
            set { _control = value; }
            get { return _control;  }
        } 
        #endregion

        #region activation functions
        // In this region the activation function are declared
        private double hlim(double v)
        {
            if (v >= this._hardLimit)
            {
                return 1.0;
            }
            else
            {
                return 0.0;
            }

        }

        private double pwl(double v)
        {
            if (v <= -_hardLimit)
            {
                return 0.0;
            }
            else
            {
                if ((v > -_hardLimit) && (v < _hardLimit))
                {
                    return v + _hardLimit;
                }
                else
                {
                    /*
                    if (v >= 0.5)
                    {
                    */
                    return 1.0;
                    //}
                }
            }
        }

        private double sig(double a, double v)
        {
            return (1 / (1 + Math.Exp((-this._hardLimit) * v)));
        }
        #endregion

        #region feedback functions
        // Calculation of the total sum of all values of the input vector
        private void CalculateInputVector()
        {
            double sum = 0.0;

            foreach (inputVectorItem inputItem in _vectorIN)
            {
                sum += inputItem.SenderPerceptron.VectorOUT * inputItem.Weight;
            }

            //this._inputVectorValue = sum;
            this._inputVectorValue = Math.Round(sum, 5);
        }

        private double totalOfAllWeightEntries()
        {
            double sum = 0.0;

            foreach (inputVectorItem inputItem in _vectorIN)
            {
                sum += inputItem.Weight;
            }

            return sum;
        }


        // The criteria function which allows the perceptron to learn
        private void CriteriaFunction(double target)
        {
            // remove not needed connections
            /*
            for  (int i = 0; i < this._vectorIN.Count; i++)
            {
                if (this.ActivationFunction != ActivationFunctionEnum.actor)
                {
                    if (Math.Abs(this._vectorIN[i].Weight) < 0.01)
                    {
                        this._vectorIN.RemoveAt(i);
                    }
                }
            }
            */

            // The learning part
            if (this._vectorIN.Count > 0)
            {
                // For single perceptron networks ok
                this._criteriaDelta = Math.Round((target - _vectorOUT) / (_vectorIN.Count * 1.125), 5);
                this.incWeightForAllActive(this._criteriaDelta);

                /*
                this._criteriaDelta = Math.Round((target / _vectorOUT), 5);
                this.multiplyWeightOfAllActive(this._criteriaDelta);
                */

            }
            else
            {
                this._criteriaDelta = 0;
            }
        }

        // This function is called by the user in order to provide feedback to the perceptron
        // After the current perceptron has adapted by calling the criteria function internally
        // it calls the feedback-method of all perceptrons connected to the input vector
        public void GiveFeedback(double target)
        {
            this._learningCounter += 1;

            // 20150913 - JMR - Self-Cloning-Function disabled
            /*
            if ((this._growingNetwork == true)&&(this._currentActivationFunction != ActivationFunctionEnum.actor)&&(this._currentActivationFunction != ActivationFunctionEnum.receptor))
            {
                switch(this._learningCounter)
                {
                    case 100:
                        this.SendCloneMeSignal();
                        break;
                    case 300:
                        if (this._onNeuronConnected == true)
                        {
                            this.RemoveOnNeuron();
                        } else
                        {
                            this.AddOnNeuron();
                        }
                        break;
                    default:
                        break;
                }
            }
            */

            this._readyForFeedback = false;

            CriteriaFunction(target);
            try
            {
                foreach (inputVectorItem inputItem in _vectorIN)
                {
                    if (inputItem.SenderPerceptron._readyForFeedback)
                    {
                        inputItem.SenderPerceptron.GiveFeedback(target);
                    }
                }
            }
            catch (Exception e)
            {

            }
        }
        #endregion

        #region manipulate weighting
        // This region contains several method for manipulating the weighting if the incoming signals.
        // Not all methods are used. The word "Active" in the name of these functions means != 0
        private double countAllActive()
        {
            double result = 0;
            foreach (inputVectorItem inputItem in _vectorIN)
            {
                if (inputItem.SenderPerceptron.VectorOUT != 0)
                {
                    result += 1.0;
                }
            }
            return result;
        }

        private void setWeightForAll(double value)
        {
            foreach (inputVectorItem inputItem in _vectorIN)
            {
                inputItem.Weight = value;
            }
        }

        private void setWeightForAllActive(double value)
        {
            foreach (inputVectorItem inputItem in _vectorIN)
            {
                if (inputItem.SenderPerceptron.VectorOUT != 0)
                {
                    inputItem.Weight = value;
                }
            }
        }

        private void incWeightForAll(double value)
        {
            foreach (inputVectorItem inputItem in _vectorIN)
            {
                inputItem.incWeight(value);
            }
        }

        private void decWeightForAll(double value)
        {
            foreach (inputVectorItem inputItem in _vectorIN)
            {
                inputItem.decWeight(value);
            }
        }

        private void decWeightForAllActive(double value)
        {
            foreach(inputVectorItem inputItem in _vectorIN)
            {
                if (inputItem.SenderPerceptron.VectorOUT != 0)
                {
                    inputItem.decWeight(value);
                }
            }
        }

        private void incWeightForAllActive(double value)
        {
            foreach (inputVectorItem inputItem in _vectorIN)
            {
                if (inputItem.SenderPerceptron.VectorOUT != 0)
                {
                    inputItem.incWeight(value);
                }
            }
        }

        private void multiplyWeightOfAllActive(double value)
        {
            foreach (inputVectorItem inputItem in _vectorIN)
            {
                if (inputItem.SenderPerceptron.VectorOUT != 0)
                {
                    inputItem.multiplyWeight(value);
                }
            }
        }

        private void incWeightForAllActiveDependingOnWeight(double error, double sumOfWeight)
        {
            foreach (inputVectorItem inputItem in _vectorIN)
            {
                if (inputItem.SenderPerceptron.VectorOUT != 0)
                {
                    inputItem.incWeight(error / sumOfWeight * inputItem.Weight);
                }
            }
        }

        private void setWeightForAllActiveDependingOnWeight(double error, double sumOfWeight)
        {
            foreach (inputVectorItem inputItem in _vectorIN)
            {
                if (inputItem.SenderPerceptron.VectorOUT != 0)
                {
                    inputItem.Weight = error / sumOfWeight * inputItem.Weight;
                }
            }
        }

        #endregion

        #region eventfiring / handling

        public delegate void delegateFiredBy(Perceptron p);
        public event delegateFiredBy FiredBy;

        // In this region the methods and handlers for the FireAction-Event is defined.
        // On this event the perceptrons in th einput vector register. It is fired by these perceptrons 
        // every time, when they are stimulated externally or the actionpotential of is changed by other
        // incoming signals
        public delegate void delegateFireAction();
        public event delegateFireAction FireAction;

        private void timerEventHandler(object sender, EventArgs e)
        {
            this._latencySemaphore += 1;                                    
        }

        private void FireEvent()
        {
            // Dieser Ausstieg verhindert ein EventFeuerwerk, gleichzusetzen mit einem
            // epileptischen Anfall
            if (this._latencySemaphore > 0) return;

            CalculateInputVector();

            if (FireAction != null)
            {

                // Latenzroutine zur Synchronisation und Impulssammlung über eine 
                // definierte Anzahl Rechenzyklen hinweg
                System.Windows.Forms.Timer t = new System.Windows.Forms.Timer();
                t.Tick += timerEventHandler;
                t.Enabled = true;
                this._latencySemaphore = 0;
                while (this._latencySemaphore < this._latency)
                {
                    this._latencySemaphore += 1;
                    Application.DoEvents();
                }
                t.Enabled = false;
                t.Dispose();

                switch (this._currentActivationFunction)
                {
                    case ActivationFunctionEnum.hlim:
                        this._vectorOUT = hlim(_inputVectorValue);
                        break;
                    case ActivationFunctionEnum.pwl:
                        this._vectorOUT = pwl(_inputVectorValue);
                        break;
                    case ActivationFunctionEnum.sig:
                        this._vectorOUT = sig(5, _inputVectorValue);
                        break;
                    case ActivationFunctionEnum.actor:
                        this._vectorOUT = this.VectorIN;
                        break;
                    case ActivationFunctionEnum.buffer:
                        //this._vectorOUT = this.VectorIN;
                        this._dBuffer.Add(this.VectorIN);
                        break;
                    default:
                        break;
                }
                FireAction();
            }

            if (FiredBy != null)
            {
                FiredBy(this);
            }

            this._readyForFeedback = true;
            this._latencySemaphore = 0;

        }


        // 20150913 - JMR - Self-Cloning-Function disabled
        /*
        public delegate void delegateCreatePerceptronCopy(Perceptron perceptronToClone);
        public event delegateCreatePerceptronCopy CloneMe;

        public void SendCloneMeSignal()
        {
            if (CloneMe != null)
            {
                CloneMe(this);
            }
        }
        */

        #endregion

        #region graph/network building
        // With this add-function a perceptron is added to the input-vecotr of the current perceptron
        public bool addInputChannel(Perceptron inputPerceptron, double weight)
        {
            if (inputPerceptron == this) return false;

            if (_currentActivationFunction != ActivationFunctionEnum.receptor)
            {
                inputVectorItem newItem = new inputVectorItem(inputPerceptron, weight);
                newItem.SenderPerceptron.FireAction += this.FireEvent;
                this._vectorIN.Add(newItem);
            }
            return true;
        }

        public bool addInputChannel(Perceptron inputPerceptron)
        {
            return this.addInputChannel(inputPerceptron, 1.0);
        }

        #endregion

            #region tools
            // This method is used to stimulate a perceptron, so that it can be used a a receptor cell
        public void stimulate(double value)
        {
            if (FireAction != null)
            {
                this._vectorOUT = value;
                FireAction();
            }
        }
        #endregion
    }
}
#endregion